package org.box.metadata.cli.impl;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Options;
import org.box.metadata.cli.annotation.Command;
import org.box.metadata.cli.exception.ParseException;
import org.box.metadata.cli.util.Util;

//TODO: provide abstraction for all CLIParsers, not only for GNU
/**
 * Provides GnuParser implementation with Commands' support.
 * 
 * @see CommandCompatible
 * 
 * @author <a href="mailto:alexander.box@gmail.com">Alexander Korobka</a>
 * 
 */
public class GnuCommandsSupportParser extends GnuParser implements
		CommandCompatible {

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.box.metadata.cli.CommandCompatible#parse(org.box.metadata.cli.Commands
	 * , java.lang.String[])
	 */
	public CommandLine parse(boolean incCommonOptions, Class<?> cfgClass, String[] args)
			throws ParseException {

		Command command = findCommand(cfgClass, args);
		Options options = (command == null) ? 
				Util.createCLIOptions(cfgClass) 
				: Util.createCLIOptions(incCommonOptions, command, cfgClass);
				
		CommandLine cmd;
		try {
			cmd = super.parse(options, excludeCommandName(command, args)); 
		} catch (org.apache.commons.cli.ParseException e) {
			throw new ParseException(e.getMessage());
		}
		return cmd;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.box.metadata.cli.CommandCompatible#parseSubCommand(org.box.metadata
	 * .cli.Commands, java.lang.String[])
	 */
	public Command findCommand(Class<?> cfgClass, String[] args) {
		Set<Command> commands = Util.createCLICommands(cfgClass);

		if (commands.size() > 0 && args != null && args.length > 0) {
			return findCommandInternal(commands, args);
		}

		return null;
	}

	/**
	 * The comparator of matched commands by command's complexion
	 */
	private static final Comparator<Command> MATCHING_COMPARATOR = new Comparator<Command>() {

		public int compare(Command c1, Command c2) {
			int l1 = c1.fullName().split("\\s").length;
			int l2 = c2.fullName().split("\\s").length;

			return l1 == l2 ? 0 : l1 - l2;
		}
	};

	/**
	 * Finds a command in the given arguments
	 * 
	 * @param commands
	 *            - a list of commands to a check
	 * @param args
	 *            - a list of args to a check
	 * 
	 * @return a matched command or null
	 */
	private Command findCommandInternal(Set<Command> commands, String[] args) {
		
		for (Command c : commands) {
			if (c.shortName().equals(args[0])) 
				return c;
		}

		Queue<Command> matched = new PriorityQueue<Command>(10,
				MATCHING_COMPARATOR);

		for (Command command : commands) {
			if (Util.arrayStartsWith(args, command.fullName().split("\\s"))) {
				matched.add(command);
			}
		}

		return matched.poll();
	}

	/**
	 * A hack for the delegated parser. Excludes string(s) that represents
	 * commands from the list of arguments.
	 * 
	 * @param command
	 *            - a command that must be excluded from the args
	 * @param args
	 *            - the given args to an exclude.
	 * 
	 * @return a new list of args with no command within
	 */
	private String[] excludeCommandName(Command command, String[] args) {
		if (command == null) {
			return args;
		}
		
		int length = (command.shortName().equals(args[0])) 
				? 1 : command.fullName().split("\\s").length;
		
		String[] result = new String[args.length - length];
		System.arraycopy(args, length, result, 0, result.length);
		return result;
	}

}
